/**
 * ajax封装工具库
 * 支持：fetch api和XMLHttpRequest请求方式
 * 支持：timeout和abort机制
 * 支持：GET POST PUT DELETE OPTIONS
 * 返回Promise对象，于是支持async和await异步编程方式
 * @author ChenMingbing
 * @since 18/04/12
 */

import * as qs from './query-string';
import { isString, isPlainObject } from './check-types';

// 绝对地址正则，匹配以下绝对地址
// `http://xxx`
// `https://xxx`
// `//xxx`
const ABSOLUTE_PATH_REG = /^(?:http:|https:)?\/\/(.*?)\//i;

/**
 * 根据html5 fetch api请求数据
 * @param  {String} url      api地址
 * @param  {Object} settings 请求设置选项
 * @return {Promise}         返回一个Promise对象
 */
const ajaxByFetch = (url, settings) => {
  let tasks = [];

  let timeoutTimer = null;
  const clearTimeout = () => {
    timeoutTimer && clearTimeout(timeoutTimer);
    timeoutTimer = null;
  };

  // 超时限制
  if (settings.timeout) {
    tasks.push(
      new Promise((resolve, reject) => {
        timeoutTimer = setTimeout(() => {
          let err = new Error('fetch timeout');
          err.code = 1001; // #1001: 客户端错误代码，请求超时
          reject(err);
        }, settings.timeout);
      })
    );
  }

  // 取消请求
  let abort = null;
  tasks.push(
    new Promise((resolve, reject) => {
      abort = () => {
        let err = new Error('fetch aborted');
        err.code = 1002; // #1002: 客户端错误代码，请求被取消
        reject(err);
        clearTimeout();
      };
    })
  );

  tasks.push(
    window
      .fetch(url, settings)
      .then(res => {
        clearTimeout();

        // <200||>=300异常http status，抛出错误
        if (res.status < 200 || (res.status >= 300 && res.status !== 304)) {
          let err = new Error(`${res.status} ${res.statusText}`);
          err.code = res.status;
          throw err;
        }

        if (settings.dataType === 'json') {
          return res.json().catch(() => {});
        }
        return res.text().catch(() => '');
      })
      .then(result => {
        if (settings.dataType === 'json') {
          result = parseResponseData(result);
          if (!result.success) {
            let err = new Error(result.errormsg || '未知服务端错误');
            if (result.errorstack) err.stack = result.errorstack;
            err.code = result.errorcode;
            throw err;
          }
          return result.data;
        }
        return result;
      })
      .catch(err => {
        clearTimeout();
        throw err;
      })
  );

  // fetch、timeout、abort三个promise，任意一个率先改变状态，即表示请求结束
  let promise = Promise.race(tasks);
  // 挂载abort方法，便于外部手动abort请求
  promise.abort = abort;
  return promise;
};

/**
 * 根据XMLHttpRequest请求数据
 * @param  {String} url      api地址
 * @param  {Object} data     POST|PUT要提交的数据
 * @param  {Object} settings 请求设置选项
 * @return {Promise}         返回一个Promise对象
 */
const ajaxByXHR = (url, data, settings) => {
  let xhr = new XMLHttpRequest();

  // 包装Promise异步模式
  let promise = new Promise((resolve, reject) => {
    // 超时限制
    if (settings.timeout) {
      xhr.timeout = settings.timeout;
      xhr.ontimeout = () => {
        let err = new Error('xhr timeout');
        err.code = 1001; // #1001: 客户端错误代码，请求超时
        reject(err);
      };
    }

    xhr.onload = () => {
      if (xhr.readyState === 4) {
        // <200||>=300异常http status，抛出错误
        if (xhr.status < 200 || (xhr.status >= 300 && xhr.status !== 304)) {
          let err = new Error(`${xhr.status} ${xhr.statusText}`);
          err.code = xhr.status;
          reject(err);
          return;
        }

        if (settings.dataType === 'json') {
          let res = xhr.responseText ? JSON.parse(xhr.responseText) : {};
          let result = parseResponseData(res);
          if (!result.success) {
            let err = new Error(result.errormsg || '未知服务端错误');
            if (result.errorstack) err.stack = result.errorstack;
            err.code = result.errorcode;
            reject(err);
            return;
          }
          resolve(result.data);
          return;
        }

        resolve(xhr.responseText || '');
      }
    };

    xhr.onerror = () => {
      let err = new Error('未知客户端错误');
      err.code = 1000;
      reject(err);
    };

    xhr.onabort = () => {
      let err = new Error('xhr aborted');
      err.code = 1002; // #1002: 客户端错误代码，请求被取消
      reject(err);
    };

    xhr.open(settings.method, url);

    if (settings.withCredentials) {
      xhr.withCredentials = true;
    }

    for (let name in settings.headers) {
      xhr.setRequestHeader(name, settings.headers[name]);
    }

    if (settings.onUploadProgress) {
      xhr.upload.addEventListener('progress', settings.onUploadProgress, false);
    }

    if (settings.onProgress) {
      xhr.upload.addEventListener(
        'progress',
        evt => {
          settings.onProgress(evt.loaded, evt.total);
        },
        false
      );
    }

    xhr.send(data);
  });

  // 挂载abort方法，便于外部手动abort请求
  promise.abort = () => xhr.abort();
  if (settings && settings.proxy) {
    settings.proxy.abort = () => xhr.abort();
  }

  return promise;
};

/**
 * ajax
 * @param  {string} url          api地址
 * @param  {Object} data         POST|PUT请求所要传递的数据
 * @param  {Object} [options={}] 其他数据选项，比如`method` `headers`等
 *         options: {
 *          method: 'GET'|'POST'|'PUT'|'DELETE',
 *          contentType: 'application/json'|false,
 *          dataType: 'json',
 *          processData: true|false,
 *          useAjax: true|false,
 *          cors: true|false,
 *          withCredentials: true|false,
 *          cache: 'default'|'no-store'|'reload'|'no-cache'|'force-cache'|'only-if-cached',
 *          timeout: 0,
 *          headers: {},
 *          onProgress: () => {}
 *         }
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常，也可以用于async和await语法
 */
export const ajax = (url, data, options = {}) => {
  let {
    method = 'GET',
    headers = {},
    contentType = 'application/json',
    dataType = 'json',
    processData = true,
    useAjax = false,
    cors = false,
    withCredentials = false,
    cache = 'default',
    timeout = 0,
    proxy,
  } = options;

  if (ABSOLUTE_PATH_REG.test(url)) {
    let domain = RegExp.$1;
    // 当api接口域名与当前页面域名不一致，说明存在跨域请求
    // 跨域请求，设置cors属性为true
    if (domain !== location.host) cors = true;
  }

  let mime = {
    json: 'application/json',
    text: 'text/plain',
  }[dataType];

  headers = Object.assign({ Accept: mime || '*/*' }, headers);

  // 需要添加Content-Type
  if (contentType !== false) {
    headers['Content-Type'] = contentType;
  }

  // GET请求带了data参数，需要格式化成query-string
  if (method === 'GET' && data) {
    if (isString(data)) {
      url += /^\?/.test(data) ? data : `?${data}`;
    } else if (isPlainObject(data)) {
      url += `?${qs.stringify(data)}`;
    }
    data = null;
  }

  // 需要处理数据，JSON格式化
  if (processData !== false && data) {
    data = JSON.stringify(data);
  }

  let settings = {
    method,
    headers,
    cache,
    dataType,
    timeout,
    onUploadProgress: options.onUploadProgress,
    onProgress: options.onProgress,
    proxy,
  };

  // html5 fetch api
  if (window.fetch && !useAjax) {
    // settings.headers['X-Requested-With'] = 'fetch'

    // fix: Edge浏览器不能给fetch body赋值null，否则会报错
    // 因此，只当data不为空时，赋值给fetch body属性
    if (data) settings.body = data;

    if (!cors) {
      // 非跨域请求，需要发送凭证
      settings.credentials = 'include';
    } else {
      // 跨域请求，设置为`cors`模式
      settings.mode = 'cors';
    }

    return ajaxByFetch(url, settings);
  }

  // no support fetch api, use XMLHttpRequest
  // settings.headers['X-Requested-With'] = 'XMLHttpRequest';

  // 需要发送凭证
  if (withCredentials) {
    settings.withCredentials = true;
  }

  return ajaxByXHR(url, data, settings);
};

/**
 * get data
 * @param  {String} url          '/api/user/1234'
 * @param  {String|Object} [data]  query参数，可以是格式化后的query-string，也可以是未格式化的query对象
 * @param  {Object} [options={}] 额外数据选项，例如 `headers`等
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常
 * @example
 * const { get } from 'common/ajax';
 * get('/api/user/1234')
 *  .then(user => console.log(user))
 *  .catch(err => console.log(err))
 */
export const get = (url, data, options = {}) => ajax(url, data, options);

/**
 * post data
 * @param  {String} url          '/api/user/add'
 * @param  {Object} data         提交到服务器的数据
 * @param  {Object} [options={}] 额外数据选项，例如 `headers`等
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常
 * @example
 * const { post } from 'common/ajax';
 * post('/api/user/add', { name: 'xxx' })
 *  .then(id => console.log(id))
 *  .catch(err => console.log(err))
 */
export const post = (url, data, options = {}) => {
  options.method = 'POST';
  return ajax(url, data, options);
};

/**
 * 上传数据包
 * 支持跨域上传（根据域名与站点域名来判断是否是跨域上传，若是跨域上传，自动添加跨域的属性和头部）
 * @param  {String} url          上传服务
 * @param  {FormData} data       数据包（FormData对象）
 * @param  {Object} [options={}] 额外的数据选项
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常
 */
export const upload = (url, data, options = {}) => {
  options.contentType = false; // 不添加Content-Type
  options.processData = false; // 不处理data
  return post(url, data, options);
};

/**
 * put data
 * @param  {String} url          '/api/user/update'
 * @param  {Object} data         提交到服务器的数据
 * @param  {Object} [options={}] 额外数据选项，例如 `headers`等
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常
 * @example
 * const { put } from 'common/ajax';
 * put('/api/user/update', { name: 'xxx' })
 *  .then(id => console.log(id))
 *  .catch(err => console.log(err))
 */
export const put = (url, data, options = {}) => {
  options.method = 'PUT';
  return ajax(url, data, options);
};

/**
 * del data
 * @param  {String} url          '/api/user/update'
 * @param  {Object} data         提交到服务器的数据
 * @param  {Object} [options={}] 额外数据选项，例如 `headers`等
 * @return {Promise}             返回一个Promise对象，调用方自行捕捉异常
 * @example
 * const { del } from 'common/ajax';
 * del('/api/user/update', { name: 'xxx' })
 *  .then(id => console.log(id))
 *  .catch(err => console.log(err))
 */
export const del = (url, data, options = {}) => {
  options.method = 'DELETE';
  return ajax(url, data, options);
};

// 解析接口返回的数据结构
// 包装统一的结构
const parseResponseData = (data = {}) => {
  if (data.hasOwnProperty('error_code')) {
    return {
      success: !data.error_code,
      errorcode: data.error_code,
      errormsg: data.error,
      data: data.result,
    };
  }
  if (data.hasOwnProperty('success')) return data;
  return { success: true, errorcode: 0, errormsg: null, data };
};
